Skip to content

fix: gateway returns 401 on hash mismatch - Add InvalidKeyError typed…#437

Merged
greatest0fallt1me merged 2 commits into
CalloraOrg:mainfrom
Biokes:bug/gateway-prefix-hash-mismatch
Jun 25, 2026
Merged

fix: gateway returns 401 on hash mismatch - Add InvalidKeyError typed…#437
greatest0fallt1me merged 2 commits into
CalloraOrg:mainfrom
Biokes:bug/gateway-prefix-hash-mismatch

Conversation

@Biokes

@Biokes Biokes commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

When a request arrived with an API key whose prefix matched a registered key but whose full-key hash did not, gatewayRoutes surfaced an unhandled exception as a 500 Internal Server Error. This fix ensures the correct 401 Unauthorized is returned with no information leaking whether the prefix existed.

Root Cause

gatewayRoutes.ts used apiKeys.get(apiKeyHeader) — a direct Map lookup by the raw key string. It had no concept of prefix-based candidate filtering + hash verification, so any exception in the auth path fell through to the outer catch and produced a 500.

Changes

src/repositories/apiKeyRepository.ts

  • Added exported InvalidKeyError class (code: INVALID_KEY) thrown when a prefix is found but the hash does not match — makes the failure mode explicit for internal observability without leaking it to clients
  • Updated verify() to return null for no-prefix-match and throw InvalidKeyError for prefix-found-but-hash-wrong

src/routes/gatewayRoutes.ts

  • Added resolveApiKey() helper: prefix scan with timingSafeEqual + SHA-256 constant-time hash comparison
  • Returns { record } on success or { error: 'not_found' | 'hash_mismatch' } on failure
  • Both error variants map to the same UnauthorizedError(401) so the distinction is never client-observable
  • Replaces the previous apiKeys.get(apiKeyHeader) entirely

src/routes/gatewayRoutes.test.ts

  • New describe block: gateway route - API key prefix / hash mismatch (bug #421)
  • 6 tests: hash mismatch → 401, no prefix → 401, exact match → 402 (billing stub), wrong apiId → 401, revoked key → 403, identical 401 body for both error paths (no timing oracle via body)

Security Notes

  • Raw key string is never compared character-by-character; always hashed first
  • timingSafeEqual used throughout
  • Both not_found and hash_mismatch produce identical status, code, and message — prefix enumeration is not possible

Checklist

  • Mismatch returns 401 not 500
  • No timing oracle observable
  • Tests cover all auth decision paths
  • InvalidKeyError typed for internal observability only

Closes #421

… sentinel to apiKeyRepository so callers can distinguish prefix-found-but-hash-wrong from key-not-found without leaking that distinction to clients - Replace naive apiKeys.get(header) in gatewayRoutes with resolveApiKey(): prefix scan + SHA-256 constant-time hash comparison (mirrors the pattern in gatewayApiKeyAuth.ts); both 'not_found' and 'hash_mismatch' map to the same UnauthorizedError(401) so no timing oracle is observable - Add Jest test suite covering: hash mismatch -> 401, no-prefix -> 401, exact match -> passes auth (402 from billing stub), wrong apiId -> 401, revoked key -> 403, identical 401 body for mismatch vs unknown prefix Closes CalloraOrg#421
@drips-wave

drips-wave Bot commented Jun 25, 2026

Copy link
Copy Markdown

@Biokes Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@greatest0fallt1me greatest0fallt1me merged commit d823c70 into CalloraOrg:main Jun 25, 2026
1 check failed
@greatest0fallt1me

Copy link
Copy Markdown
Contributor

good fix — returning a proper 401 with a typed InvalidKeyError on hash mismatch is the right behavior. merged after a small rebase onto main. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gatewayRoutes returns 500 instead of 401 when API key prefix exists but hash mismatches

2 participants